<template>
  <div ref="waterfallContainer" class="waterfall-container">
    <draggable-container
      v-show="preLeft"
      v-model="leftTemp"
      key="left"
      :options="dragOptions"
      :containerStyle="getTempColStyle('left')"
    >
      <template #default="{ item }">
        <slot name="item" :item="item" />
      </template>
    </draggable-container>
    <template v-for="(_, columnIndex) of layout">
      <draggable-container
        v-model="layout[columnIndex]"
        :key="columnIndex"
        :options="dragOptions"
        :containerStyle="getColStyle(columnIndex)"
        @start="handleStartDrag"
        @end="handleEndDrag"
      >
        <template #default="{ item }">
          <slot name="item" :item="item" />
        </template>
      </draggable-container>
    </template>
    <draggable-container
      v-show="preRight"
      v-model="rightTemp"
      key="right"
      :itemKey="itemKey"
      :options="dragOptions"
      :containerStyle="getTempColStyle('right')"
    >
      <template #default="{ item }">
        <slot name="item" :item="item" />
      </template>
    </draggable-container>
  </div>
</template>

<script>
import { debounce, throttle } from "lodash";
import waterfallProps from "./waterfallProps";
import DraggableContainer from "./draggableContainer.vue";
import { DragManager } from "./dragManager";
import { cloneDeep } from "lodash";
import dragCacheManager from "./dragCacheManager";

export default {
  name: "WaterfallLayout",
  components: { DraggableContainer },
  props: waterfallProps,
  created() {
    this.cacheManager = new dragCacheManager(this.id, this.itemKey); // 缓存管理
    this.dragManager = new DragManager(); // 拖拽管理

    this.historyLayout = []; // 历史布局
    this.unLayoutItems = []; // 除历史布局外的元素
    this.storeLayout = []; // 存储编辑前的视图

    this.currentDrawId = 0; // 绘制任务的标识符，主要用于等高列瀑布流渲染时判断
    this.resizeObserver = null; // 视图大小变化监听器
  },
  data() {
    return {
      layout: [], // 实际布局
      draggable: false, // 是否支持拖拽
      preLeft: false, // 是否显示左侧新增列
      leftTemp: [], // 左侧新增列数据
      preRight: false, // 是否新增右侧新增列
      rightTemp: [], // 右侧预增列数据
    };
  },
  computed: {
    dragOptions() {
      return {
        group: `draggable-container-${this.id}`,
        disabled: !this.draggable,
      };
    },
    gapX() {
      const gapX = this.gap === "number" ? this.gap : this.gap[0];
      return `${gapX / 2}px`; // 左右均分边距
    },
    gapY() {
      const gapY = this.gap === "number" ? this.gap : this.gap[1];
      return `${gapY}px`;
    },
    commonColStyle() {
      return {
        gap: `${this.gapY}`,
        "min-width": `${this.minWidth}px`,
      };
    },
  },
  mounted() {
    this.initialize();
  },
  beforeDestroy() {
    this.disconnectObserveResize();
    this.unWatchMouseOver();
  },
  watch: {
    items: {
      deep: true,
      handler() {
        if (this.draggable === false) {
          // 拖拽模式下，不变化数据
          this.initialize();
        }
      },
    },
    cols(value) {
      if (typeof value === "object") {
        this.handleColumnCountChange();
        this.resizeObserver();
        return;
      }
      this.disconnectObserveResize();
    },
  },
  methods: {
    /**
     * 开始编辑
     */
    edit() {
      this.draggable = true;
      this.storeLayout = cloneDeep(this.layout);
      this.disconnectObserveResize();
    },

    /**
     * 确认编辑
     */
    okEdit() {
      this.draggable = false;
      this.storeLayout = [];
      if (this.history) {
        this.cacheManager.cache(this.layout);
        this.historyLayout = this.layout;
      }
    },

    /**
     * 取消编辑
     */
    cancelEdit() {
      this.draggable = false;
      this.layout = this.storeLayout;
      this.storeLayout = [];
      this.resizeObserver();
    },

    /**
     * 恢复布局
     */
    revertLayout() {
      this.cacheManager.clearCache();
      this.initialize();
    },

    /**
     * 获取列样式
     */
    getColStyle(index) {
      index = this.preLeft ? index + 1 : index;
      const colLength =
        Number(this.preLeft) + this.layout.length + Number(this.preRight);
      return {
        ...this.commonColStyle,
        "padding-left": index !== 0 ? this.gapX : 0,
        "padding-right": index !== colLength - 1 ? this.gapX : 0,
        "border-left":
          index !== 0 && this.divider ? "1px solid #eee" : undefined,
      };
    },

    getTempColStyle(position) {
      return {
        ...this.commonColStyle,
        "padding-left": position !== "left" ? this.gapX : 0,
        "padding-right": position !== "right" ? this.gapX : 0,
        "border-left":
          position !== "left" && this.divider ? "1px solid #eee" : undefined,
      };
    },

    /**
     * 初始化布局
     */
    initialize() {
      this.initializeLayoutData();
      this.draw();
      this.observeResize();
    },

    /**
     * 初始化布局数据
     */
    initializeLayoutData() {
      const cache = this.history ? this.cacheManager.getCache() : [];
      const itemMap = new Map(
        this.items.map((item) => [item[this.itemKey], item])
      );

      const historyLayout = Array.from({ length: cache.length }, () => []);

      for (let colIndex = 0; colIndex < cache.length; colIndex++) {
        for (let key of cache[colIndex]) {
          if (!itemMap.has(key)) {
            continue;
          }
          historyLayout[colIndex].push(itemMap.get(key));
          itemMap.delete(key);
        }
      }

      this.historyLayout = historyLayout.filter((col) => !!col.length);
      this.unLayoutItems = [...itemMap.values()];
    },

    /**
     * 开始布局
     * @param items
     * @param layout
     */
    draw() {
      this.currentDrawId++;

      if (this.historyLayout.length) {
        this.layout = this.historyLayout;
      } else {
        const columnCount = this.getColumnCount();
        this.layout = Array.from({ length: columnCount }, () => []);
      }

      if (this.isEqualHeight && !this.draggable) {
        this.insertByShortest(this.currentDrawId, this.unLayoutItems);
      } else {
        this.insertSequentially(this.unLayoutItems);
      }
    },

    /**
     * 获取渲染列数
     */
    getColumnCount() {
      if (typeof this.cols === "number") {
        return Math.min(this.cols, this.items.length);
      }

      const width =
        this.$refs.waterfallContainer?.clientWidth || window.innerWidth;
      const sorted = Object.keys(this.cols)
        .map(Number)
        .sort((a, b) => a - b);

      let columnCount = 1;
      sorted.forEach((bp) => {
        if (width >= bp) {
          columnCount = this.cols[bp];
        }
      });

      if (columnCount < 0) {
        return 1;
      }
      return Math.min(columnCount, this.items.length);
    },

    /**
     * 按最短列插入
     */
    async insertByShortest(drawId, items) {
      await this.$nextTick(); // 首次计算前，也需等渲染完成后再执行
      const columnHeights = this.getInitColumnHeights();
      for (let item of items) {
        if (drawId !== this.currentDrawId) {
          // 如果当前不是最新的绘制任务，就中断执行
          return;
        }

        const shortestIndex = columnHeights.indexOf(Math.min(...columnHeights));
        this.layout[shortestIndex].push(item);

        // 在下一次 DOM 更新后，更新缓存高度
        await this.$nextTick();
        this.updateColumnHeights(columnHeights, shortestIndex);
      }
    },

    /**
     * 初始化列高度缓存
     */
    getInitColumnHeights() {
      const columnDivs = [...this.$refs.waterfallContainer.children];
      return columnDivs
        .filter((col) => col.style.display !== "none")
        .map((col) => {
          const children = [...col.children];
          return children.reduce(
            (sum, child) => sum + this.getItemHeight(child),
            0
          );
        });
    },

    /**
     * 更新列高度
     */
    updateColumnHeights(columnHeights, columnIndex) {
      const column = [...this.$refs.waterfallContainer.children].filter(
        (col) => col.style.display !== "none"
      )[columnIndex];

      const lastChild = column.lastElementChild;
      if (lastChild) {
        columnHeights[columnIndex] += this.getItemHeight(lastChild);
      }
    },

    getItemHeight(item) {
      const style = window.getComputedStyle(item);
      const marginTop = parseFloat(style.marginTop) || 0;
      const marginBottom = parseFloat(style.marginBottom) || 0;
      return item.getBoundingClientRect().height + marginTop + marginBottom;
    },

    /**
     * 顺序插入
     */
    insertSequentially(items) {
      items.forEach((item, index) => {
        const colIndex = index % this.layout.length;
        this.layout[colIndex].push(item);
      });
    },

    /**
     * 监听容器尺寸变化
     */
    observeResize() {
      if (
        this.resizeObserver !== null ||
        typeof this.cols !== "object" ||
        this.draggable === true ||
        this.historyLayout.length !== 0
      ) {
        return;
      }

      this.resizeObserver = new ResizeObserver(
        debounce(this.handleColumnCountChange)
      );
      this.resizeObserver.observe(this.$refs.waterfallContainer);
    },

    /**
     * 处理列数变化变化
     */
    handleColumnCountChange() {
      if (this.draggable || this.historyLayout.length !== 0) {
        return;
      }

      const oldColumnCount = this.layout.length;
      const columnCount = this.getColumnCount();

      if (columnCount !== oldColumnCount) {
        this.draw();
      }
    },

    /**
     * 移除监听容器尺寸变化
     */
    disconnectObserveResize() {
      if (this.resizeObserver) {
        this.resizeObserver.disconnect();
        this.resizeObserver = null;
      }
    },

    /**
     * 开始拖拽
     */
    handleStartDrag() {
      const container = this.$refs.waterfallContainer;
      const containerRect = container.getBoundingClientRect();
      this.dragManager.startDrag(containerRect.left, containerRect.width);
      this.watchMouseOver();
    },

    /**
     * 原生 H5 拖拽无法修改拖拽元素的样式，使用 forceFallback 模式
     * 该模式下实际没有 dragover 事件，改用 mouseover 鼠标事件代替
     */
    watchMouseOver() {
      document.addEventListener("mouseover", this.handleContainerDragOver);
    },

    /**
     * 结束拖拽
     */
    async handleEndDrag() {
      this.unWatchMouseOver();
      this.dragManager.endDrag();

      this.preLeft = false;
      this.preRight = false;
      await this.$nextTick();

      this.layout = this.layout.filter((items) => !!items.length);
      if (this.leftTemp.length !== 0) {
        this.layout = [this.leftTemp, ...this.layout];
        this.leftTemp = [];
      }
      if (this.rightTemp.length !== 0) {
        this.layout = [...this.layout, this.rightTemp];
        this.rightTemp = [];
      }
    },

    unWatchMouseOver() {
      document.removeEventListener("mouseover", this.handleContainerDragOver);
    },

    /**
     * 判断是否悬停在视图边缘位置
     */
    handleContainerDragOver: throttle(function (event) {
      if (this.dragManager.draggable === false) {
        return;
      }
      this.doHandleContainerDragOver(event);
    }, 100),

    doHandleContainerDragOver(event) {
      // 是否靠近左侧列
      if (this.dragManager.nearContainerLeft(event)) {
        this.preLeft = true;
      } else {
        this.preLeft = false;
      }

      // 是否靠近右侧列
      if (this.dragManager.nearContainerRight(event)) {
        this.preRight = true;
      } else {
        this.preRight = false;
      }
    },
  },
};
</script>

<style scoped>
.waterfall-container {
  display: flex;
}

.draggable-container {
  display: flex;
  flex-direction: column;
  flex-basis: 400px;
  flex-grow: 1;
  min-width: 0px;
  height: 100%;
}
</style>
